Introduction
I this notebook we ingest and visualize the mobility trends data provided by Apple, [APPL1].
We take the following steps:
Download the data
Import the data and summarise it
Transform the data into long form
Partition the data into subsets that correspond to combinations of geographical regions and transportation types
Make contingency matrices and corresponding heat-map plots
Make nearest neighbors graphs over the contingency matrices and plot communities
Plot the corresponding time series
Data description
About This Data The CSV file and charts on this site show a relative volume of directions requests per country/region or city compared to a baseline volume on January 13th, 2020. We define our day as midnight-to-midnight, Pacific time. Cities represent usage in greater metropolitan areas and are stably defined during this period. In many countries/regions and cities, relative volume has increased since January 13th, consistent with normal, seasonal usage of Apple Maps. Day of week effects are important to normalize as you use this data. Data that is sent from users’ devices to the Maps service is associated with random, rotating identifiers so Apple doesn’t have a profile of your movements and searches. Apple Maps has no demographic information about our users, so we can’t make any statements about the representativeness of our usage against the overall population.
Observations
The observations listed in this subsection are also placed under the relevant statistics in the following sections and indicated with “Observation”.
The directions requests volumes reference date for normalization is 2020-01-13 : all the values in that column are \(100\).
From the community clusters of the nearest neighbor graphs (derived from the time series of the normalized driving directions requests volume) we see that countries and cities are clustered in expected ways. For example, in the community graph plot corresponding to “{city, driving}” the cities Oslo, Copenhagen, Helsinki, Stockholm, and Zurich are placed in the same cluster. In the graphs corresponding to “{city, transit}” and “{city, walking}” the Japanese cities Tokyo, Osaka, Nagoya, and Fukuoka are clustered together.
In the time series plots the Sundays are indicated with orange dashed lines. We can see that from Monday to Thursday people are more familiar with their trips than say on Fridays and Saturdays. We can also see that on Sundays people (on average) are more familiar with their trips or simply travel less.
Load packages
library(Matrix)
library(tidyverse)
library(ggplot2)
library(gridExtra)
library(d3heatmap)
library(igraph)
library(zoo)
library(forecast)
Data ingestion
Apple mobile data was provided in this WWW page: https://www.apple.com/covid19/mobility , [APPL1]. (The data has to be download from that web page – there is an “agreement to terms”, etc.)
dfAppleMobility <- read.csv( "~/Downloads/applemobilitytrends-2021-08-14.csv", stringsAsFactors = FALSE)
#dfAppleMobility <- read.csv( "~/Downloads/applemobilitytrends-2021-02-20.csv", stringsAsFactors = FALSE)
#dfAppleMobility <- read.csv("https://covid19-static.cdn-apple.com/covid19-mobility-data/2024HotfixDev18/v3/en-us/applemobilitytrends-2021-01-15.csv")
names(dfAppleMobility) <- gsub( "^X", "", names(dfAppleMobility))
names(dfAppleMobility) <- gsub( ".", "-", names(dfAppleMobility), fixed = TRUE)
dfAppleMobility
Observation: The directions requests volumes reference date for normalization is 2020-01-13 : all the values in that column are \(100\).
Data dimensions:
dim(dfAppleMobility)
[1] 4691 586
Data summary:
summary(as.data.frame(unclass(dfAppleMobility[,1:3]), stringsAsFactors = TRUE))
geo_type region transportation_type
city : 790 Washington County: 27 driving:3048
country/region: 153 Jefferson County : 25 transit: 551
county :2638 Montgomery County: 24 walking:1092
sub-region :1110 Franklin County : 22
Madison County : 21
Jackson County : 19
(Other) :4553
Number of unique “country/region” values:
dfAppleMobility %>%
dplyr::filter( geo_type == "country/region") %>%
dplyr::pull("region") %>%
unique %>%
length
[1] 63
Number of unique “city” values:
dfAppleMobility %>%
dplyr::filter( geo_type == "city") %>%
dplyr::pull("region") %>%
unique %>%
length
[1] 295
All unique geo types:
lsGeoTypes <- unique(dfAppleMobility[["geo_type"]])
lsGeoTypes
[1] "country/region" "city" "sub-region" "county"
All unique transportation types:
lsTransportationTypes <- unique(dfAppleMobility[["transportation_type"]])
lsTransportationTypes
[1] "driving" "walking" "transit"
Data transformation
It is better to have the data in long form (narrow form). For that I am using the package “tidyr”.
# lsIDColumnNames <- c("geo_type", "region", "transportation_type") # For the initial dataset released by Apple.
lsIDColumnNames <- c("geo_type", "region", "transportation_type", "alternative_name", "sub-region", "country" )
dfAppleMobilityLongForm <- tidyr::pivot_longer( data = dfAppleMobility, cols = setdiff( names(dfAppleMobility), lsIDColumnNames), names_to = "Date", values_to = "Value" )
dim(dfAppleMobilityLongForm)
[1] 2720780 8
Remove the rows with “empty” values:
dfAppleMobilityLongForm <- dfAppleMobilityLongForm[ complete.cases(dfAppleMobilityLongForm), ]
dim(dfAppleMobilityLongForm)
[1] 2686336 8
Add the “DateObject” column:
dfAppleMobilityLongForm$DateObject <- as.POSIXct( dfAppleMobilityLongForm$Date, format = "%Y-%m-%d", origin = "1970-01-01" )
Add “day name” (“day of the week”) field:
dfAppleMobilityLongForm$DayName <- weekdays(dfAppleMobilityLongForm$DateObject)
Here is sample of the transformed data:
set.seed(3232)
dfAppleMobilityLongForm %>% dplyr::sample_n( 10 )
Here is summary:
summary(as.data.frame(unclass(dfAppleMobilityLongForm), stringsAsFactors = TRUE))
geo_type region transportation_type alternative_name sub.region country Date Value DateObject DayName
city : 455838 Washington County: 15589 driving:1736193 :2098328 : 764407 United States:1791138 2020-01-13: 4652 Min. : 0.44 Min. :2020-01-13 00:00:00 Friday :381464
country/region: 88281 Jefferson County : 14437 transit: 318585 AB : 1735 Texas : 139133 Japan : 127705 2020-01-14: 4652 1st Qu.: 90.23 1st Qu.:2020-06-06 00:00:00 Monday :382530
county :1523222 Montgomery County: 13866 walking: 631558 ACT : 1735 California: 95890 : 88281 2020-01-15: 4652 Median : 121.43 Median :2020-10-29 00:00:00 Saturday :386116
sub-region : 618995 Franklin County : 12698 Andalucía : 1735 Georgia : 75629 France : 51998 2020-01-16: 4652 Mean : 130.75 Mean :2020-10-28 17:37:42 Sunday :381464
Madison County : 12123 Bayern : 1735 Virginia : 71013 Germany : 49668 2020-01-17: 4652 3rd Qu.: 159.41 3rd Qu.:2021-03-23 00:00:00 Thursday :386116
Jackson County : 10969 BC|Colombie-Britannique: 1735 Florida : 69903 Thailand : 39248 2020-01-18: 4652 Max. :2148.12 Max. :2021-08-14 00:00:00 Tuesday :382530
(Other) :2606654 (Other) : 579333 (Other) :1470361 (Other) : 538298 (Other) :2658424 Wednesday:386116
Partition the data into geo types × transportation types:
dfAppleMobilityLongForm %>%
dplyr::group_by( geo_type, transportation_type) %>%
dplyr::count()
aQueries <- split(dfAppleMobilityLongForm, dfAppleMobilityLongForm[,c("geo_type", "transportation_type")] )
Heat-map plots
We can visualize the data using heat-map plots.
Remark: Using the contingency matrices prepared for the heat-map plots we can do further analysis, like, finding correlations or nearest neighbors. (See below.)
Cross-tabulate dates with regions:
aMatDateRegion <- purrr::map( aQueries, function(dfX) { xtabs( formula = Value ~ Date + region, data = dfX, sparse = TRUE ) } )
aMatDateRegion <- aMatDateRegion[ purrr::map_lgl(aMatDateRegion, function(x) nrow(x) > 0 ) ]
dfPlotQuery <- purrr::map_df( aMatDateRegion, Matrix::summary, .id = "Type" )
head(dfPlotQuery)
577 x 295 sparse Matrix of class "dgCMatrix", with 170215 entries
Type i j x
1 city.driving 1 1 100.00
2 city.driving 2 1 100.73
3 city.driving 3 1 102.86
4 city.driving 4 1 102.65
5 city.driving 5 1 109.39
6 city.driving 6 1 109.62
ggplot2::ggplot(dfPlotQuery) +
ggplot2::geom_tile( ggplot2::aes( x = j, y = i, fill = log10(x)), color = "white") +
ggplot2::scale_fill_gradient(low = "white", high = "blue") +
ggplot2::xlab("Region") + ggplot2::ylab("Date") +
ggplot2::facet_wrap( ~Type, scales = "free", ncol = 2)

Here we take a “closer look” to one of the plots using a dedicated d3heatmap plot:
d3heatmap::d3heatmap( x = aMatDateRegion[["country/region.driving"]], Rowv = FALSE )
Warning in RColorBrewer::brewer.pal(n, pal) :
n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning in RColorBrewer::brewer.pal(n, pal) :
n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Nearest neighbors graphs
Graphs overview
Here we create nearest neighbor graphs of the contingency matrices computed above and plot cluster the nodes:
th <- 0.94
aNNGraphs <-
purrr::map( aMatDateRegion, function(m) {
m2 <- cor(as.matrix(m))
for( i in 1:nrow(m2) ) {
m2[i,i] <- 0
}
m2 <- as( m2, "dgCMatrix")
m2@x[ m2@x <= th ] <- 0
#m2@x[ m2@x > th ] <- 1
igraph::graph_from_adjacency_matrix(Matrix::drop0(m2), weighted = TRUE, mode = "undirected")
})
ind <- 3
ceb <- cluster_edge_betweenness(aNNGraphs[[ind]])
dendPlot(ceb, mode="hclust", main = names(aNNGraphs)[[ind]])
plot(ceb, aNNGraphs[[ind]], vertex.size=1, vertex.label=NA, main = names(aNNGraphs)[[ind]])
Time series analysis
Time series
In this section for each date we sum all cases over the region-transportation pairs, make a time series, and plot them.
Remark: In the plots the Sundays are indicated with orange dashed lines.
Here we make the time series:
aDateStringToDateObject <- unique( dfAppleMobilityLongForm[, c("Date", "DateObject")] )
aDateStringToDateObject <- setNames( aDateStringToDateObject$DateObject, aDateStringToDateObject$Date )
aDateStringToDateObject <- as.POSIXct(aDateStringToDateObject)
aTSDirReqByCountry <- purrr::map( aMatDateRegion, function(m) rowSums(m) )
matTS <- do.call( cbind, aTSDirReqByCountry)
Warning in (function (..., deparse.level = 1) :
number of rows of result is not a multiple of vector length (arg 1)
zooObj <- zoo::zoo( x = matTS, as.POSIXct(rownames(matTS)) )
Here we plot them:
autoplot(zooObj) +
aes(colour = NULL, linetype = NULL) +
facet_grid(Series ~ ., scales = "free_y") +
geom_vline( xintercept = aDateStringToDateObject[weekdays(aDateStringToDateObject) == "Sunday"], color = "orange", linetype = "dashed", size = 0.3 )

Observation: In the time series plots the Sundays are indicated with orange dashed lines. We can see that from Monday to Thursday people are more familiar with their trips than say on Fridays and Saturdays. We can also see that on Sundays people (on average) are more familiar with their trips or simply travel less.
“Forecast”
He we do “forecast” for code-workflow demonstration purposes – the forecasts should not be taken seriously.
Fit a time series model to the time series:
aTSModels <- purrr::map( names(zooObj), function(x) { forecast::auto.arima( zoo( x = zooObj[,x], order.by = index(zooObj) ) ) } )
aTSModels <- purrr::map( names(zooObj), function(x) forecast::forecast( as.matrix(zooObj)[,x] ) )
names(aTSModels) <- names(zooObj)
Plot data and forecast:
lsPlots <- purrr::map( names(aTSModels), function(x) autoplot(aTSModels[[x]]) + ylab("Volume") + ggtitle(x) )
names(lsPlots) <- names(aTSModels)
do.call( gridExtra::grid.arrange, lsPlots )

LS0tCnRpdGxlOiAiQXBwbGUgbW9iaWxpdHkgdHJlbmRzIGRhdGEgdmlzdWFsaXphdGlvbiIKYXV0aG9yOiBBbnRvbiBBbnRvbm92CmRhdGU6IDIwMjAtMDUtMTMKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KLm1haW4tY29udGFpbmVyIHsKICBtYXgtd2lkdGg6IDE4MDBweDsKICBtYXJnaW4tbGVmdDogYXV0bzsKICBtYXJnaW4tcmlnaHQ6IGF1dG87Cn0KPC9zdHlsZT4KCgojIEludHJvZHVjdGlvbgoKSSB0aGlzIG5vdGVib29rIHdlIGluZ2VzdCBhbmQgdmlzdWFsaXplIHRoZSBtb2JpbGl0eSB0cmVuZHMgZGF0YSBwcm92aWRlZCBieSBBcHBsZSwgW0FQUEwxXS4KCldlIHRha2UgdGhlIGZvbGxvd2luZyBzdGVwczoKCjEuIERvd25sb2FkIHRoZSBkYXRhCgoyLiBJbXBvcnQgdGhlIGRhdGEgYW5kIHN1bW1hcmlzZSBpdAoKMy4gVHJhbnNmb3JtIHRoZSBkYXRhIGludG8gbG9uZyBmb3JtCgo0LiBQYXJ0aXRpb24gdGhlIGRhdGEgaW50byBzdWJzZXRzIHRoYXQgY29ycmVzcG9uZCB0byBjb21iaW5hdGlvbnMgb2YgZ2VvZ3JhcGhpY2FsIHJlZ2lvbnMgYW5kIHRyYW5zcG9ydGF0aW9uIHR5cGVzCgo1LiBNYWtlIGNvbnRpbmdlbmN5IG1hdHJpY2VzIGFuZCBjb3JyZXNwb25kaW5nIGhlYXQtbWFwIHBsb3RzCgo2LiBNYWtlIG5lYXJlc3QgbmVpZ2hib3JzIGdyYXBocyBvdmVyIHRoZSBjb250aW5nZW5jeSBtYXRyaWNlcyBhbmQgcGxvdCBjb21tdW5pdGllcwoKNy4gUGxvdCB0aGUgY29ycmVzcG9uZGluZyB0aW1lIHNlcmllcwoKIyMgRGF0YSBkZXNjcmlwdGlvbgoKIyMjIEZyb20gQXBwbGXigJlzIHBhZ2UgW2h0dHBzOi8vd3d3LmFwcGxlLmNvbS9jb3ZpZDE5L21vYmlsaXR5XShodHRwczovL3d3dy5hcHBsZS5jb20vY292aWQxOS9tb2JpbGl0eSkKCioqQWJvdXQgVGhpcyBEYXRhKioKVGhlIENTViBmaWxlIGFuZCBjaGFydHMgb24gdGhpcyBzaXRlIHNob3cgYSByZWxhdGl2ZSB2b2x1bWUgb2YgZGlyZWN0aW9ucyByZXF1ZXN0cyBwZXIgY291bnRyeS9yZWdpb24gb3IgY2l0eSBjb21wYXJlZCB0byBhIGJhc2VsaW5lIHZvbHVtZSBvbiBKYW51YXJ5IDEzdGgsIDIwMjAuCldlIGRlZmluZSBvdXIgZGF5IGFzIG1pZG5pZ2h0LXRvLW1pZG5pZ2h0LCBQYWNpZmljIHRpbWUuIENpdGllcyByZXByZXNlbnQgdXNhZ2UgaW4gZ3JlYXRlciBtZXRyb3BvbGl0YW4gYXJlYXMgYW5kIGFyZSBzdGFibHkgZGVmaW5lZCBkdXJpbmcgdGhpcyBwZXJpb2QuIEluIG1hbnkgY291bnRyaWVzL3JlZ2lvbnMgYW5kIGNpdGllcywgcmVsYXRpdmUgdm9sdW1lIGhhcyBpbmNyZWFzZWQgc2luY2UgSmFudWFyeSAxM3RoLCBjb25zaXN0ZW50IHdpdGggbm9ybWFsLCBzZWFzb25hbCB1c2FnZSBvZiBBcHBsZSBNYXBzLiBEYXkgb2Ygd2VlayBlZmZlY3RzIGFyZSBpbXBvcnRhbnQgdG8gbm9ybWFsaXplIGFzIHlvdSB1c2UgdGhpcyBkYXRhLgpEYXRhIHRoYXQgaXMgc2VudCBmcm9tIHVzZXJz4oCZIGRldmljZXMgdG8gdGhlIE1hcHMgc2VydmljZSBpcyBhc3NvY2lhdGVkIHdpdGggcmFuZG9tLCByb3RhdGluZyBpZGVudGlmaWVycyBzbyBBcHBsZSBkb2VzbuKAmXQgaGF2ZSBhIHByb2ZpbGUgb2YgeW91ciBtb3ZlbWVudHMgYW5kIHNlYXJjaGVzLiBBcHBsZSBNYXBzIGhhcyBubyBkZW1vZ3JhcGhpYyBpbmZvcm1hdGlvbiBhYm91dCBvdXIgdXNlcnMsIHNvIHdlIGNhbuKAmXQgbWFrZSBhbnkgc3RhdGVtZW50cyBhYm91dCB0aGUgcmVwcmVzZW50YXRpdmVuZXNzIG9mIG91ciB1c2FnZSBhZ2FpbnN0IHRoZSBvdmVyYWxsIHBvcHVsYXRpb24uCgojIyBPYnNlcnZhdGlvbnMKClRoZSBvYnNlcnZhdGlvbnMgbGlzdGVkIGluIHRoaXMgc3Vic2VjdGlvbiBhcmUgYWxzbyBwbGFjZWQgdW5kZXIgdGhlIHJlbGV2YW50IHN0YXRpc3RpY3MgaW4gdGhlIGZvbGxvd2luZyBzZWN0aW9ucyBhbmQgaW5kaWNhdGVkIHdpdGgg4oCcKipPYnNlcnZhdGlvbioq4oCdLgoKLSBUaGUgZGlyZWN0aW9ucyByZXF1ZXN0cyB2b2x1bWVzIHJlZmVyZW5jZSBkYXRlIGZvciBub3JtYWxpemF0aW9uIGlzIDIwMjAtMDEtMTMgOiBhbGwgdGhlIHZhbHVlcyBpbiB0aGF0IGNvbHVtbiBhcmUgJDEwMCQuCgotIEZyb20gdGhlIGNvbW11bml0eSBjbHVzdGVycyBvZiB0aGUgbmVhcmVzdCBuZWlnaGJvciBncmFwaHMgKGRlcml2ZWQgZnJvbSB0aGUgdGltZSBzZXJpZXMgb2YgdGhlIG5vcm1hbGl6ZWQgZHJpdmluZyBkaXJlY3Rpb25zIHJlcXVlc3RzIHZvbHVtZSkgd2Ugc2VlIHRoYXQgY291bnRyaWVzIGFuZCBjaXRpZXMgYXJlIGNsdXN0ZXJlZCBpbiBleHBlY3RlZCB3YXlzLiBGb3IgZXhhbXBsZSwgaW4gdGhlIGNvbW11bml0eSBncmFwaCBwbG90IGNvcnJlc3BvbmRpbmcgdG8g4oCce2NpdHksIGRyaXZpbmd94oCdIHRoZSBjaXRpZXMgT3NsbywgQ29wZW5oYWdlbiwgSGVsc2lua2ksIFN0b2NraG9sbSwgYW5kIFp1cmljaCBhcmUgcGxhY2VkIGluIHRoZSBzYW1lIGNsdXN0ZXIuIEluIHRoZSBncmFwaHMgY29ycmVzcG9uZGluZyB0byDigJx7Y2l0eSwgdHJhbnNpdH3igJ0gYW5kIOKAnHtjaXR5LCB3YWxraW5nfeKAnSB0aGUgSmFwYW5lc2UgY2l0aWVzIFRva3lvLCBPc2FrYSwgTmFnb3lhLCBhbmQgRnVrdW9rYSBhcmUgY2x1c3RlcmVkIHRvZ2V0aGVyLgoKLSBJbiB0aGUgdGltZSBzZXJpZXMgcGxvdHMgdGhlIFN1bmRheXMgYXJlIGluZGljYXRlZCB3aXRoIG9yYW5nZSBkYXNoZWQgbGluZXMuIFdlIGNhbiBzZWUgdGhhdCBmcm9tIE1vbmRheSB0byBUaHVyc2RheSBwZW9wbGUgYXJlIG1vcmUgZmFtaWxpYXIgd2l0aCB0aGVpciB0cmlwcyB0aGFuIHNheSBvbiBGcmlkYXlzIGFuZCBTYXR1cmRheXMuIFdlIGNhbiBhbHNvIHNlZSB0aGF0IG9uIFN1bmRheXMgcGVvcGxlIChvbiBhdmVyYWdlKSBhcmUgbW9yZSBmYW1pbGlhciB3aXRoIHRoZWlyIHRyaXBzIG9yIHNpbXBseSB0cmF2ZWwgbGVzcy4KCiMgTG9hZCBwYWNrYWdlcwoKYGBge3J9CmxpYnJhcnkoTWF0cml4KQpsaWJyYXJ5KHRpZHl2ZXJzZSkKbGlicmFyeShnZ3Bsb3QyKQpsaWJyYXJ5KGdyaWRFeHRyYSkKbGlicmFyeShkM2hlYXRtYXApCmxpYnJhcnkoaWdyYXBoKQpsaWJyYXJ5KHpvbykKbGlicmFyeShmb3JlY2FzdCkKYGBgCgoKIyMgRGF0YSBpbmdlc3Rpb24KCkFwcGxlIG1vYmlsZSBkYXRhIHdhcyBwcm92aWRlZCBpbiB0aGlzIFdXVyBwYWdlOiBbaHR0cHM6Ly93d3cuYXBwbGUuY29tL2NvdmlkMTkvbW9iaWxpdHldKGh0dHBzOi8vd3d3LmFwcGxlLmNvbS9jb3ZpZDE5L21vYmlsaXR5KSAsIFtBUFBMMV0uIChUaGUgZGF0YSBoYXMgdG8gYmUgZG93bmxvYWQgZnJvbSB0aGF0IHdlYiBwYWdlIC0tIHRoZXJlIGlzIGFuIOKAnGFncmVlbWVudCB0byB0ZXJtc+KAnSwgZXRjLikKCmBgYHtyfQpkZkFwcGxlTW9iaWxpdHkgPC0gcmVhZC5jc3YoICJ+L0Rvd25sb2Fkcy9hcHBsZW1vYmlsaXR5dHJlbmRzLTIwMjEtMDgtMTQuY3N2Iiwgc3RyaW5nc0FzRmFjdG9ycyA9IEZBTFNFKQojZGZBcHBsZU1vYmlsaXR5IDwtIHJlYWQuY3N2KCAifi9Eb3dubG9hZHMvYXBwbGVtb2JpbGl0eXRyZW5kcy0yMDIxLTAyLTIwLmNzdiIsIHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSkKI2RmQXBwbGVNb2JpbGl0eSA8LSByZWFkLmNzdigiaHR0cHM6Ly9jb3ZpZDE5LXN0YXRpYy5jZG4tYXBwbGUuY29tL2NvdmlkMTktbW9iaWxpdHktZGF0YS8yMDI0SG90Zml4RGV2MTgvdjMvZW4tdXMvYXBwbGVtb2JpbGl0eXRyZW5kcy0yMDIxLTAxLTE1LmNzdiIpCm5hbWVzKGRmQXBwbGVNb2JpbGl0eSkgPC0gZ3N1YiggIl5YIiwgIiIsIG5hbWVzKGRmQXBwbGVNb2JpbGl0eSkpCm5hbWVzKGRmQXBwbGVNb2JpbGl0eSkgPC0gZ3N1YiggIi4iLCAiLSIsIG5hbWVzKGRmQXBwbGVNb2JpbGl0eSksIGZpeGVkID0gVFJVRSkKYGBgCgpgYGB7cn0KZGZBcHBsZU1vYmlsaXR5CmBgYAoKCioqT2JzZXJ2YXRpb246KiogVGhlIGRpcmVjdGlvbnMgcmVxdWVzdHMgdm9sdW1lcyByZWZlcmVuY2UgZGF0ZSBmb3Igbm9ybWFsaXphdGlvbiBpcyAyMDIwLTAxLTEzIDogYWxsIHRoZSB2YWx1ZXMgaW4gdGhhdCBjb2x1bW4gYXJlICQxMDAkLgoKRGF0YSBkaW1lbnNpb25zOgoKYGBge3J9CmRpbShkZkFwcGxlTW9iaWxpdHkpCmBgYAoKRGF0YSBzdW1tYXJ5OgoKYGBge3J9CnN1bW1hcnkoYXMuZGF0YS5mcmFtZSh1bmNsYXNzKGRmQXBwbGVNb2JpbGl0eVssMTozXSksIHN0cmluZ3NBc0ZhY3RvcnMgPSBUUlVFKSkKYGBgCgpOdW1iZXIgb2YgdW5pcXVlIOKAnGNvdW50cnkvcmVnaW9u4oCdIHZhbHVlczoKCmBgYHtyfQpkZkFwcGxlTW9iaWxpdHkgJT4lIAogIGRwbHlyOjpmaWx0ZXIoIGdlb190eXBlID09ICJjb3VudHJ5L3JlZ2lvbiIpICU+JSAKICBkcGx5cjo6cHVsbCgicmVnaW9uIikgJT4lCiAgdW5pcXVlICU+JSAKICBsZW5ndGgKYGBgCgpOdW1iZXIgb2YgdW5pcXVlIOKAnGNpdHnigJ0gdmFsdWVzOgoKYGBge3J9CmRmQXBwbGVNb2JpbGl0eSAlPiUgCiAgZHBseXI6OmZpbHRlciggZ2VvX3R5cGUgPT0gImNpdHkiKSAlPiUgCiAgZHBseXI6OnB1bGwoInJlZ2lvbiIpICU+JQogIHVuaXF1ZSAlPiUgCiAgbGVuZ3RoCmBgYAoKCkFsbCB1bmlxdWUgZ2VvIHR5cGVzOgoKYGBge3J9CmxzR2VvVHlwZXMgPC0gdW5pcXVlKGRmQXBwbGVNb2JpbGl0eVtbImdlb190eXBlIl1dKQpsc0dlb1R5cGVzCmBgYAoKQWxsIHVuaXF1ZSB0cmFuc3BvcnRhdGlvbiB0eXBlczoKCmBgYHtyfQpsc1RyYW5zcG9ydGF0aW9uVHlwZXMgPC0gIHVuaXF1ZShkZkFwcGxlTW9iaWxpdHlbWyJ0cmFuc3BvcnRhdGlvbl90eXBlIl1dKQpsc1RyYW5zcG9ydGF0aW9uVHlwZXMKYGBgCgojIERhdGEgdHJhbnNmb3JtYXRpb24KCkl0IGlzIGJldHRlciB0byBoYXZlIHRoZSBkYXRhIGluIFtsb25nIGZvcm0gKG5hcnJvdyBmb3JtKV0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvV2lkZV9hbmRfbmFycm93X2RhdGEpLiAKRm9yIHRoYXQgSSBhbSB1c2luZyB0aGUgcGFja2FnZSBbInRpZHlyIl0oaHR0cHM6Ly90aWR5ci50aWR5dmVyc2Uub3JnKS4KCmBgYHtyfQojIGxzSURDb2x1bW5OYW1lcyA8LSBjKCJnZW9fdHlwZSIsICJyZWdpb24iLCAidHJhbnNwb3J0YXRpb25fdHlwZSIpICMgRm9yIHRoZSBpbml0aWFsIGRhdGFzZXQgcmVsZWFzZWQgYnkgQXBwbGUuCmxzSURDb2x1bW5OYW1lcyA8LSBjKCJnZW9fdHlwZSIsICJyZWdpb24iLCAidHJhbnNwb3J0YXRpb25fdHlwZSIsICJhbHRlcm5hdGl2ZV9uYW1lIiwgInN1Yi1yZWdpb24iLCAiY291bnRyeSIgKQpkZkFwcGxlTW9iaWxpdHlMb25nRm9ybSA8LSB0aWR5cjo6cGl2b3RfbG9uZ2VyKCBkYXRhID0gZGZBcHBsZU1vYmlsaXR5LCBjb2xzID0gc2V0ZGlmZiggbmFtZXMoZGZBcHBsZU1vYmlsaXR5KSwgbHNJRENvbHVtbk5hbWVzKSwgbmFtZXNfdG8gPSAiRGF0ZSIsIHZhbHVlc190byA9ICJWYWx1ZSIgKQpkaW0oZGZBcHBsZU1vYmlsaXR5TG9uZ0Zvcm0pCmBgYAoKUmVtb3ZlIHRoZSByb3dzIHdpdGgg4oCcZW1wdHnigJ0gdmFsdWVzOgoKYGBge3J9CmRmQXBwbGVNb2JpbGl0eUxvbmdGb3JtIDwtIGRmQXBwbGVNb2JpbGl0eUxvbmdGb3JtWyBjb21wbGV0ZS5jYXNlcyhkZkFwcGxlTW9iaWxpdHlMb25nRm9ybSksIF0KZGltKGRmQXBwbGVNb2JpbGl0eUxvbmdGb3JtKQpgYGAKCkFkZCB0aGUgIkRhdGVPYmplY3QiIGNvbHVtbjoKCmBgYHtyfQpkZkFwcGxlTW9iaWxpdHlMb25nRm9ybSREYXRlT2JqZWN0IDwtIGFzLlBPU0lYY3QoIGRmQXBwbGVNb2JpbGl0eUxvbmdGb3JtJERhdGUsIGZvcm1hdCA9ICIlWS0lbS0lZCIsIG9yaWdpbiA9ICIxOTcwLTAxLTAxIiApCmBgYAoKQWRkICJkYXkgbmFtZSIgKOKAnGRheSBvZiB0aGUgd2Vla+KAnSkgZmllbGQ6CgpgYGB7cn0KZGZBcHBsZU1vYmlsaXR5TG9uZ0Zvcm0kRGF5TmFtZSA8LSB3ZWVrZGF5cyhkZkFwcGxlTW9iaWxpdHlMb25nRm9ybSREYXRlT2JqZWN0KQpgYGAKCkhlcmUgaXMgc2FtcGxlIG9mIHRoZSB0cmFuc2Zvcm1lZCBkYXRhOgoKYGBge3J9CnNldC5zZWVkKDMyMzIpCmRmQXBwbGVNb2JpbGl0eUxvbmdGb3JtICU+JSBkcGx5cjo6c2FtcGxlX24oIDEwICkKYGBgCgpIZXJlIGlzIHN1bW1hcnk6CgpgYGB7cn0Kc3VtbWFyeShhcy5kYXRhLmZyYW1lKHVuY2xhc3MoZGZBcHBsZU1vYmlsaXR5TG9uZ0Zvcm0pLCBzdHJpbmdzQXNGYWN0b3JzID0gVFJVRSkpCmBgYAoKUGFydGl0aW9uIHRoZSBkYXRhIGludG8gZ2VvIHR5cGVzIMOXIHRyYW5zcG9ydGF0aW9uIHR5cGVzOgoKYGBge3J9CmRmQXBwbGVNb2JpbGl0eUxvbmdGb3JtICU+JSAKICBkcGx5cjo6Z3JvdXBfYnkoIGdlb190eXBlLCB0cmFuc3BvcnRhdGlvbl90eXBlKSAlPiUgCiAgZHBseXI6OmNvdW50KCkKYGBgCgpgYGB7cn0KYVF1ZXJpZXMgPC0gc3BsaXQoZGZBcHBsZU1vYmlsaXR5TG9uZ0Zvcm0sICBkZkFwcGxlTW9iaWxpdHlMb25nRm9ybVssYygiZ2VvX3R5cGUiLCAidHJhbnNwb3J0YXRpb25fdHlwZSIpXSApCmBgYAoKIyBIZWF0LW1hcCBwbG90cwoKV2UgY2FuIHZpc3VhbGl6ZSB0aGUgZGF0YSB1c2luZyBoZWF0LW1hcCBwbG90cy4KCioqUmVtYXJrOioqIFVzaW5nIHRoZSBjb250aW5nZW5jeSBtYXRyaWNlcyBwcmVwYXJlZCBmb3IgdGhlIGhlYXQtbWFwIHBsb3RzIHdlIGNhbiBkbyBmdXJ0aGVyIGFuYWx5c2lzLCBsaWtlLCBmaW5kaW5nIGNvcnJlbGF0aW9ucyBvciBuZWFyZXN0IG5laWdoYm9ycy4gKFNlZSBiZWxvdy4pCgpDcm9zcy10YWJ1bGF0ZSBkYXRlcyB3aXRoIHJlZ2lvbnM6CgpgYGB7cn0KYU1hdERhdGVSZWdpb24gPC0gcHVycnI6Om1hcCggYVF1ZXJpZXMsIGZ1bmN0aW9uKGRmWCkgeyB4dGFicyggZm9ybXVsYSA9IFZhbHVlIH4gRGF0ZSArIHJlZ2lvbiwgZGF0YSA9IGRmWCwgc3BhcnNlID0gVFJVRSApIH0gKQphTWF0RGF0ZVJlZ2lvbiA8LSBhTWF0RGF0ZVJlZ2lvblsgcHVycnI6Om1hcF9sZ2woYU1hdERhdGVSZWdpb24sIGZ1bmN0aW9uKHgpIG5yb3coeCkgPiAwICkgXQpgYGAKCgoKYGBge3J9CmRmUGxvdFF1ZXJ5IDwtIHB1cnJyOjptYXBfZGYoIGFNYXREYXRlUmVnaW9uLCBNYXRyaXg6OnN1bW1hcnksIC5pZCA9ICJUeXBlIiApCmhlYWQoZGZQbG90UXVlcnkpCmBgYAoKYGBge3IsIGZpZy53aWR0aCA9IDgsIGZpZy5oaWdodCA9IDgsIHdhcm5pbmc9RkFMU0V9CmdncGxvdDI6OmdncGxvdChkZlBsb3RRdWVyeSkgKwogIGdncGxvdDI6Omdlb21fdGlsZSggZ2dwbG90Mjo6YWVzKCB4ID0gaiwgeSA9IGksIGZpbGwgPSBsb2cxMCh4KSksIGNvbG9yID0gIndoaXRlIikgKwogIGdncGxvdDI6OnNjYWxlX2ZpbGxfZ3JhZGllbnQobG93ID0gIndoaXRlIiwgaGlnaCA9ICJibHVlIikgKwogIGdncGxvdDI6OnhsYWIoIlJlZ2lvbiIpICsgZ2dwbG90Mjo6eWxhYigiRGF0ZSIpICsgCiAgZ2dwbG90Mjo6ZmFjZXRfd3JhcCggflR5cGUsIHNjYWxlcyA9ICJmcmVlIiwgbmNvbCA9IDIpCmBgYAoKSGVyZSB3ZSB0YWtlIGEgImNsb3NlciBsb29rIiB0byBvbmUgb2YgdGhlIHBsb3RzIHVzaW5nIGEgZGVkaWNhdGVkIGBkM2hlYXRtYXBgIHBsb3Q6CgpgYGB7cn0KZDNoZWF0bWFwOjpkM2hlYXRtYXAoIHggPSBhTWF0RGF0ZVJlZ2lvbltbImNvdW50cnkvcmVnaW9uLmRyaXZpbmciXV0sIFJvd3YgPSBGQUxTRSApCmBgYAoKIyBOZWFyZXN0IG5laWdoYm9ycyBncmFwaHMKCiMjIEdyYXBocyBvdmVydmlldwoKSGVyZSB3ZSBjcmVhdGUgbmVhcmVzdCBuZWlnaGJvciBncmFwaHMgb2YgdGhlIGNvbnRpbmdlbmN5IG1hdHJpY2VzIGNvbXB1dGVkIGFib3ZlIGFuZCBwbG90IGNsdXN0ZXIgdGhlIG5vZGVzOgoKYGBge3J9CnRoIDwtIDAuOTQKYU5OR3JhcGhzIDwtIAogIHB1cnJyOjptYXAoIGFNYXREYXRlUmVnaW9uLCBmdW5jdGlvbihtKSB7IAogICAgbTIgPC0gY29yKGFzLm1hdHJpeChtKSkKICAgIGZvciggaSBpbiAxOm5yb3cobTIpICkgewogICAgICBtMltpLGldIDwtIDAKICAgIH0KICAgIG0yIDwtIGFzKCBtMiwgImRnQ01hdHJpeCIpIAogICAgbTJAeFsgbTJAeCA8PSB0aCBdIDwtIDAKICAgICNtMkB4WyBtMkB4ID4gdGggXSA8LSAxCiAgICBpZ3JhcGg6OmdyYXBoX2Zyb21fYWRqYWNlbmN5X21hdHJpeChNYXRyaXg6OmRyb3AwKG0yKSwgd2VpZ2h0ZWQgPSBUUlVFLCBtb2RlID0gInVuZGlyZWN0ZWQiKQogIH0pCmBgYAoKYGBge3IsIGV2YWw9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CmluZCA8LSAzCmNlYiA8LSBjbHVzdGVyX2VkZ2VfYmV0d2Vlbm5lc3MoYU5OR3JhcGhzW1tpbmRdXSkgIApkZW5kUGxvdChjZWIsIG1vZGU9ImhjbHVzdCIsIG1haW4gPSBuYW1lcyhhTk5HcmFwaHMpW1tpbmRdXSkKYGBgCgpgYGB7ciwgZXZhbD1GQUxTRX0KcGxvdChjZWIsIGFOTkdyYXBoc1tbaW5kXV0sIHZlcnRleC5zaXplPTEsIHZlcnRleC5sYWJlbD1OQSwgbWFpbiA9IG5hbWVzKGFOTkdyYXBocylbW2luZF1dKQpgYGAKCiMgVGltZSBzZXJpZXMgYW5hbHlzaXMKCiMjIFRpbWUgc2VyaWVzCgpJbiB0aGlzIHNlY3Rpb24gZm9yIGVhY2ggZGF0ZSB3ZSBzdW0gYWxsIGNhc2VzIG92ZXIgdGhlIHJlZ2lvbi10cmFuc3BvcnRhdGlvbiBwYWlycywgbWFrZSBhIHRpbWUgc2VyaWVzLCBhbmQgcGxvdCB0aGVtLiAKCioqUmVtYXJrOioqIEluIHRoZSBwbG90cyB0aGUgU3VuZGF5cyBhcmUgaW5kaWNhdGVkIHdpdGggb3JhbmdlIGRhc2hlZCBsaW5lcy4KCkhlcmUgd2UgbWFrZSB0aGUgdGltZSBzZXJpZXM6CgpgYGB7cn0KYURhdGVTdHJpbmdUb0RhdGVPYmplY3QgPC0gdW5pcXVlKCBkZkFwcGxlTW9iaWxpdHlMb25nRm9ybVssIGMoIkRhdGUiLCAiRGF0ZU9iamVjdCIpXSApCmFEYXRlU3RyaW5nVG9EYXRlT2JqZWN0IDwtIHNldE5hbWVzKCBhRGF0ZVN0cmluZ1RvRGF0ZU9iamVjdCREYXRlT2JqZWN0LCBhRGF0ZVN0cmluZ1RvRGF0ZU9iamVjdCREYXRlICkKYURhdGVTdHJpbmdUb0RhdGVPYmplY3QgPC0gYXMuUE9TSVhjdChhRGF0ZVN0cmluZ1RvRGF0ZU9iamVjdCkKYVRTRGlyUmVxQnlDb3VudHJ5IDwtICBwdXJycjo6bWFwKCBhTWF0RGF0ZVJlZ2lvbiwgZnVuY3Rpb24obSkgcm93U3VtcyhtKSApCmBgYAoKYGBge3J9Cm1hdFRTIDwtIGRvLmNhbGwoIGNiaW5kLCBhVFNEaXJSZXFCeUNvdW50cnkpCmBgYAoKYGBge3J9Cnpvb09iaiA8LSB6b286OnpvbyggeCA9IG1hdFRTLCBhcy5QT1NJWGN0KHJvd25hbWVzKG1hdFRTKSkgKQpgYGAKCkhlcmUgd2UgcGxvdCB0aGVtOgoKCmBgYHtyLCBmaWcuaGVpZ2h0PTYsIGZpZy53aWR0aD02fQphdXRvcGxvdCh6b29PYmopICsKICBhZXMoY29sb3VyID0gTlVMTCwgbGluZXR5cGUgPSBOVUxMKSArCglmYWNldF9ncmlkKFNlcmllcyB+IC4sIHNjYWxlcyA9ICJmcmVlX3kiKSArCiAgZ2VvbV92bGluZSggeGludGVyY2VwdCA9IGFEYXRlU3RyaW5nVG9EYXRlT2JqZWN0W3dlZWtkYXlzKGFEYXRlU3RyaW5nVG9EYXRlT2JqZWN0KSA9PSAiU3VuZGF5Il0sIGNvbG9yID0gIm9yYW5nZSIsIGxpbmV0eXBlID0gImRhc2hlZCIsIHNpemUgPSAwLjMgKQpgYGAKCgoqKk9ic2VydmF0aW9uOioqIEluIHRoZSB0aW1lIHNlcmllcyBwbG90cyB0aGUgU3VuZGF5cyBhcmUgaW5kaWNhdGVkIHdpdGggb3JhbmdlIGRhc2hlZCBsaW5lcy4gCldlIGNhbiBzZWUgdGhhdCBmcm9tIE1vbmRheSB0byBUaHVyc2RheSBwZW9wbGUgYXJlIG1vcmUgZmFtaWxpYXIgd2l0aCB0aGVpciB0cmlwcyB0aGFuIHNheSBvbiBGcmlkYXlzIGFuZCBTYXR1cmRheXMuIApXZSBjYW4gYWxzbyBzZWUgdGhhdCBvbiBTdW5kYXlzIHBlb3BsZSAob24gYXZlcmFnZSkgYXJlIG1vcmUgZmFtaWxpYXIgd2l0aCB0aGVpciB0cmlwcyBvciBzaW1wbHkgdHJhdmVsIGxlc3MuCgojIyDigJxGb3JlY2FzdOKAnQoKSGUgd2UgZG8g4oCcZm9yZWNhc3TigJ0gZm9yIGNvZGUtd29ya2Zsb3cgZGVtb25zdHJhdGlvbiBwdXJwb3NlcyAtLSB0aGUgZm9yZWNhc3RzIHNob3VsZCBub3QgYmUgdGFrZW4gc2VyaW91c2x5LgoKRml0IGEgdGltZSBzZXJpZXMgbW9kZWwgdG8gdGhlIHRpbWUgc2VyaWVzOgoKYGBge3J9CmFUU01vZGVscyA8LSBwdXJycjo6bWFwKCBuYW1lcyh6b29PYmopLCBmdW5jdGlvbih4KSB7IGZvcmVjYXN0OjphdXRvLmFyaW1hKCB6b28oIHggPSB6b29PYmpbLHhdLCBvcmRlci5ieSA9IGluZGV4KHpvb09iaikgKSApIH0gKQpgYGAKCmBgYHtyfQphVFNNb2RlbHMgPC0gcHVycnI6Om1hcCggbmFtZXMoem9vT2JqKSwgZnVuY3Rpb24oeCkgZm9yZWNhc3Q6OmZvcmVjYXN0KCBhcy5tYXRyaXgoem9vT2JqKVsseF0gKSApCm5hbWVzKGFUU01vZGVscykgPC0gbmFtZXMoem9vT2JqKQpgYGAKClBsb3QgZGF0YSBhbmQgZm9yZWNhc3Q6CgpgYGB7cn0KbHNQbG90cyA8LSBwdXJycjo6bWFwKCBuYW1lcyhhVFNNb2RlbHMpLCBmdW5jdGlvbih4KSBhdXRvcGxvdChhVFNNb2RlbHNbW3hdXSkgKyB5bGFiKCJWb2x1bWUiKSArIGdndGl0bGUoeCkgKQpuYW1lcyhsc1Bsb3RzKSA8LSBuYW1lcyhhVFNNb2RlbHMpCmBgYAoKCmBgYHtyfQpkby5jYWxsKCBncmlkRXh0cmE6OmdyaWQuYXJyYW5nZSwgbHNQbG90cyApCmBgYAoKIyBSZWZlcmVuY2VzCgpbQVBQTDFdIEFwcGxlIEluYy4sIFtNb2JpbGl0eSBUcmVuZHMgUmVwb3J0c10oaHR0cHM6Ly93d3cuYXBwbGUuY29tL2NvdmlkMTkvbW9iaWxpdHkpLCAoMjAyMCksIFthcHBsZS5jb21dKGh0dHBzOi8vd3d3LmFwcGxlLmNvbSkuCgpbQUExXSBBbnRvbiBBbnRvbm92LCAKWyJBcHBsZSBtb2JpbGl0eSB0cmVuZHMgZGF0YSB2aXN1YWxpemF0aW9uIl0oaHR0cHM6Ly9naXRodWIuY29tL2FudG9ub25jdWJlL1N5c3RlbU1vZGVsaW5nL2Jsb2IvbWFzdGVyL1Byb2plY3RzL0Nvcm9uYXZpcnVzLXByb3BhZ2F0aW9uLWR5bmFtaWNzL0RvY3VtZW50cy9BcHBsZS1tb2JpbGl0eS10cmVuZHMtZGF0YS12aXN1YWxpemF0aW9uLm1kKSwgCigyMDIwKSwgCltTeXN0ZW1Nb2RlbGluZyBhdCBHaXRIdWJdKGh0dHBzOi8vZ2l0aHViLmNvbS9hbnRvbm9uY3ViZS9TeXN0ZW1Nb2RlbGluZykuCgpbQUEyXSBBbnRvbiBBbnRvbm92LCAKWyJOWSBUaW1lcyBDT1ZJRC0xOSBkYXRhIHZpc3VhbGl6YXRpb24iXShodHRwczovL2dpdGh1Yi5jb20vYW50b25vbmN1YmUvU3lzdGVtTW9kZWxpbmcvYmxvYi9tYXN0ZXIvUHJvamVjdHMvQ29yb25hdmlydXMtcHJvcGFnYXRpb24tZHluYW1pY3MvRG9jdW1lbnRzL05ZVGltZXMtQ09WSUQtMTktZGF0YS12aXN1YWxpemF0aW9uLm1kKSwgCigyMDIwKSwgCltTeXN0ZW1Nb2RlbGluZyBhdCBHaXRIdWJdKGh0dHBzOi8vZ2l0aHViLmNvbS9hbnRvbm9uY3ViZS9TeXN0ZW1Nb2RlbGluZykuCgo=